#! /usr/bin/env python

"""
This module contains the ClientProxy class that allows clients to
access the distributed file system as if it was local.

As our file servers only support a flat file system its the clients
job to map pathnames to file ids.

A Console class is also provided which gives a command line console
wrapper onto the proxy allowing it to be used like a unix shell
supporting commands like pwd,cd,ls,rm,mkdir,rmdir etc.

The console also acts as the admin tool for directory location service
as well as an all round debug and test tool.

"""


import sys,socket,cmd,string
import marshal, os, io, errno
import shutil,time
import cStringIO as StringIO
import uuid

from dfs_common import *
from proxy_tools import *
from dir_server import DirSocket
from file_server import FileSocket


class LookUpResult:
    "Used to hold the result of lookup tree walk"

    def __init__(self, pfid, pftype, cfid, cftype):
        self.parent_fid = pfid
        self.parent_ftype = pftype
        self.fid = cfid
        self.ftype = cftype

    def __str__(self):
        return 'parent(%s,%d) child(%s,%d)' % (
                self.parent_fid, self.parent_ftype,
                self.fid, self.ftype)


class NameNotFound(DFSError):
    "Raised if we don't find a file name in a directory."

    def __init__(self, dfid, name):
        self.dfid = dfid
        self.name = name

    def __str__(self):
        return '%s %s' %( self.dfid, self.name)

class NameExists(DFSError):
    "Raised if we find a file name already exists in a directory."

    def __init__(self, dfid, name, efid, eftype):
        self.dfid = dfid
        self.name = name
        self.efid = efid
        self.eftype = eftype

    def __str__(self):
        return '%s %s %s %d' %( self.dfid, self.name, self.efid, self.eftype)


class ClientProxy:
    """Allows client access to the distributed file system.

    Client use the proxy to maintain both files and directories on the DFS.

    Sample usage:

        proxy = ClientProxy(addr)

        proxy.mkdir('test')
        proxy.rmdir('test')

        f = proxy.open('myfile.txt','w')
        f.write('hello world\n')
        f.close()

        f = proxy.open('myfile.txt')
        for line in f:
            print line
        f.close()
    
    """

    def __init__(self, addr=DFS_DIRADDR):
        self.dir_addr = addr
        # this tells where our file servers
        self.dir_server = None
        # connected file servers
        self.file_servers = {}
        # open files and cache file pages
        self.page_cache = PageCache(DFS_MAXPAGE)

    # helper method to shutdown client threads
    def shutdown(self):
        for conn in self.file_servers.values():
            conn.close_sock()

    def handle_deleted(self, fid):
        print 'Deleted', fid
        self.page_cache.set_fattr(fid, None)
    
    def handle_modified(self, fid, fattr):
        print 'Modified', fid, fattr
        self.page_cache.set_fattr(fid, fattr)

    # return a directory service connnection
    def get_dserver(self):
        # first try existing connection
        dserver = self.dir_server
        if not dserver or dserver.closed():
            # create a new one
            try:
                dserver = DirSocket(self.dir_addr)
            except socket.error, e:
                raise ConnError(e.errno, addr2str(self.dir_addr))
            self.dir_server = dserver
            dserver.start()
        return dserver
  
    # 
    # verify if root directory exists 
    # we auto confgure root directory file
    #
    def check_rootdir(self, fserver):
        # ensure root dir exists
        try:
            fattr = fserver.stat(DFS_ROOTDIR)
        except RequestError, e:
            # only create if not found error
            if e.rc != errno.ENOENT:
                raise OSError(errno.ENOENT, 'No root!', DFS_ROOTDIR)
            # so create it
            try:
                fid,fattr = fserver.create_wfid(DFS_ROOTDIR, FTYPE_DIR)
            except RequestError, e:
                raise OSError(e.rc, 'No root!', DFS_ROOTDIR)

    # get a file server connection for the given address
    def get_fserver(self, addr):
        # look up existing server
        key = addr2str(addr)
        fserver = self.file_servers.get(key,None)

        if not fserver or fserver.closed():
            try:
                fserver = FileSocket(addr)   
            except socket.error, e:
                raise ConnError(e.errno, key)
            self.file_servers[key] = fserver
            fserver.set_event_handler(self)
            fserver.start()
            self.check_rootdir(fserver)

        return fserver

    # internal close
    def close_file(self, afile):
        try:
            # tell cache one less active user
            refs = self.page_cache.close(afile.fid)
            if refs == 0:
                # should we bother ?
                rc = afile.fserver.unregister(afile.fid)
        except ConnError:
            pass
        finally:
            # release lock if tid set
            if afile.tid:
                self.unlock(afile.tid)

    #
    # internal read (this is our kernel layer)
    #
    # we always read from the cache first. 
    # if the requrired page is not found we read a PAGESIZE block of data
    # from the from the file server and update the memory cache
    # before returning the data back to the calling File object
    #
    def read_file(self, buffers, afile, pos, size):
        page_num,offset,page_pos = pos2page(pos)

        total_read = 0
        now = time.time()

        # loop till done
        while 1:
            #print 'read-page', page_num, page_pos, offset, DFS_PAGESIZE - offset
            page = self.page_cache.get_page(afile.fid, page_num) 

            # check if page is stale (call back should do this)
            if page and (now - page.checked_time) >= DFS_INTERVAL:
                fattr = afile.fserver.stat(afile.fid)
                # update file state (discard cache if old)
                if self.page_cache.set_fattr(afile.fid, fattr):
                    self.page_cache.set_checked(afile.fid, now)
                else:
                    page = None

            # fetch page if necesary from server
            if not page:
                fattr,data = afile.fserver.get(afile.fid, page_pos, DFS_PAGESIZE)
                #print 'read-load', page_num, page_pos, 0, len(data)
                # eof check
                if not data: break
                # update file state (discard cache if old)
                self.page_cache.set_fattr(afile.fid, fattr)
                # store it
                page = Page(afile.fid, page_num, data, time.time())
                self.page_cache.set_page(page)

            data = page.get_data()
            dlen = len(data)
            # required = min(avail, remain)
            rlen = min(dlen - offset, size - total_read)
            # update read buffers
            buffers.append(data[offset:offset+rlen])
            total_read += rlen
            # stop if all read
            if total_read >= size: break
            # stop if eof
            if dlen < DFS_PAGESIZE: break
            # read next page
            page_num += 1
            page_pos += DFS_PAGESIZE
            offset = 0
        return total_read

    #
    # internal write (this is our kernel layer)
    #
    # we cache the write into our memory cache before sending it
    # to the file server. Note we only cache writes that either
    # whole PAGESIZE block or can be merged safely with existing
    # cached data. 
    #
    # Note the file server will always give the file attributes
    # that existed before and after a write.
    # We can then confirm if this matches our previous file view
    # and prevent from wiping the cache from writes to the server
    # carried out by us.
    # 
    
    def write_file(self, afile, pos, data):
        page_num,offset,page_pos = pos2page(pos)
        total_write = 0
        now = time.time()
        # loop till done
        while total_write < len(data):
            # calculate next block of data to write out
            remain = len(data) - total_write
            wlen = min(DFS_PAGESIZE, remain) - offset
            # get existing page entry
            page = self.page_cache.get_page(afile.fid, page_num)
            if page:
                # is write adjacent to or intersect cache data (no memory holes)
                if offset <= len(page.data):
                    # yes safe to merge
                    array = bytearray(page.get_data())
                    array[offset:offset+wlen] = data[total_write:total_write+wlen]
                    page.set_data(bytes(array))
                else:
                    # cannot merge - must discard
                    self.page_cache.discard_page(page)
            else:
                # new page only if starting from 0
                if offset == 0:
                    page = Page(afile.fid, page_num, data[total_write:total_write+wlen], now)
                    self.page_cache.set_page(page)
            # write data to server
            try:
                fattr = afile.fserver.put(afile.fid, page_pos + offset, data[total_write:total_write+wlen])
            except RequestError, e:
                # i/o error - must invalidate cache
                self.page_cache.set_fattr(afile.fid, None)
                raise IOError(e.rc, os.strerror(e.rc), afile.fid)
            total_write += wlen
            # update cache: will invalidate if servers prev file attr != ours
            self.page_cache.set_fattr(afile.fid, fattr[0], fattr[1])
            # next page
            page_num += 1
            page_pos += DFS_PAGESIZE
            offset = 0

    #
    # internal open (this is our kernel layer)
    #
    # return a python file like object back to caller
    #
    def open_file(self, fserver, fid, ftype, tid, mode, truncate=False):
        if truncate:
            afid,fattr = fserver.create_wfid(fid, ftype)
            self.page_cache.set_fattr(fid, fattr)

        # update cache
        refs = self.page_cache.open(fid)
        if refs == 1:
            # first time we have seen this file
            fattr = fserver.register(fid)
            self.page_cache.set_fattr(fid, fattr)
        else:
            # try cache (we do this to stop rentry by an append)
            fattr = self.page_cache.get_fattr(fid)
            if fattr is None:
                # no fetch from server
                fattr = fserver.stat(fid)
                self.page_cache.set_fattr(fid, fattr)

        return File(self, fserver, fid, tid, mode)

    #
    # update a directory file with a new file list
    #
    def set_filelist(self, fserver, dfid, file_list):
        data = ''
        for fid,ftype,name in file_list:
            astr = '%s %d %s\n' %(fid,ftype,name)
            data = data + astr

        # we must rewite the file (truncate+update)
        lock_tid = self.lock_file(dfid, 'write')
        try:
            f = self.open_file(fserver, dfid, FTYPE_DIR, None, 'w', True)
            try:
                f.write(data)
            finally:
                f.close()
        finally:
            # unlock file
            self.unlock(lock_tid)
        
    #
    # read a file list from a directory file
    #
    def get_filelist(self, fserver, dfid):
        # read file - must lock to prevent concurrent updates
        lock_tid = self.lock_file(dfid, 'read')
        try:
            f = self.open_file(fserver, dfid, FTYPE_DIR, None, 'r')
            try:
                data = f.read()
            finally:
                f.close()
        finally:
            # unlock file
            self.unlock(lock_tid)

        buf = StringIO.StringIO(data)
        file_list = []
        for line in buf:
            fields = line.split()
            fid   = fields[0]
            ftype = int(fields[1])
            name = fields[2]
            file_list.append((fid,ftype,name))
        return file_list
    
    # find the index of name in a file list
    def find_pos(self, file_list, name):
        idx = 0
        while idx < len(file_list):
            fid,ftype,fname = file_list[idx]
            if fname == name:
                return idx
            idx = idx + 1
        return -1
    
    # add a new filename to a directory
    def add_name(self, fserver, dfid, name, fid, ftype):
        # read directory file
        file_list = self.get_filelist(fserver, dfid)
        pos = self.find_pos(file_list, name)
        # check if it already exists
        if pos >= 0:
            entry = file_list[pos]
            raise NameExists(dfid, name, entry[0],entry[1])
        # add and update
        file_list.append((fid,ftype,name))
        self.set_filelist(fserver, dfid, file_list)
        
    #
    # remove a filename from a directory
    #
    def remove_name(self, fserver, dfid, name):
        # read directory file
        file_list = self.get_filelist(fserver, dfid)
        pos = self.find_pos(file_list, name)
        # check if not in list
        if pos < 0:
            raise NameNotFound(dfid, name)
        # remove and update
        del file_list[pos]
        self.set_filelist(fserver, dfid, file_list)

    #
    # get directory file entry
    #
    def lookup_name(self, fserver, dfid, name):
        # read directory file
        file_list = self.get_filelist(fserver, dfid)
        pos = self.find_pos(file_list, name)
        if pos < 0:
            raise NameNotFound(dfid, name)
        # return entry
        return file_list[pos]

    #
    # convert pathname to file id
    #
    def lookup(self, path):
        # ask dserver for fsaddr+path
        dserver = self.get_dserver()
        fsaddr, fspath = dserver.lookup(path)
  
        # get fserver conn 
        fserver = self.get_fserver(fsaddr)
        
        # walk the tree 
        parts = split_path(fspath)
        parent_fid = DFS_ROOTDIR
        parent_ftype = FTYPE_DIR
        child_fid = DFS_ROOTDIR
        child_ftype = FTYPE_DIR
        for name in parts:
            # make current child the parent
            parent_fid = child_fid
            parent_ftype = child_ftype
            # fetch next child
            child_fid,child_ftype,fname = self.lookup_name(fserver, parent_fid, name)
        
        result = LookUpResult(parent_fid, parent_ftype, child_fid, child_ftype)
      
        # we don't put fserver in result
        return fserver, result

    # 
    # confirm file type is actually a directory
    #
    def check_dir(self, path, ftype):
        if ftype != FTYPE_DIR:
            rc = errno.ENOTDIR
            raise OSError(rc, os.strerror(rc), path)

    # helper method (for proxy console)
    def lookup_path(self, path):
        
        try:
            fserver, result = self.lookup(path)
        except NameNotFound, e:
            rc = errno.ENOENT
            raise OSError(rc, os.strerror(rc), path)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), path)

        return '%s %s' %( str(fserver),str(result) )

    def mkdir(self, path):
        "Create a directory named path."
        try:
            dirname = get_dirname(path)
            fserver, result = self.lookup(dirname)
            self.check_dir(path, result.ftype)
            name = get_basename(path)
            new_fid,fattr = fserver.create(result.ftype)
            # add directory default entries
            file_list = [ (result.fid, result.ftype, '..' ) ]
            self.set_filelist(fserver, new_fid, file_list)
            # now update parent
            self.add_name(fserver, result.fid, name, new_fid, result.ftype)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except NameExists, e:
            fserver.remove(new_fid)
            rc = errno.EEXIST
            raise OSError(rc, os.strerror(rc), path)

    def rmdir(self, path): 
        "Remove (delete) a directory named path."
        try:
            fserver, result = self.lookup(path)
            self.check_dir(path, result.ftype)
            # joker check
            if result.fid == DFS_ROOTDIR:
                rc = errno.EACCES 
                raise OSError(rc, os.strerror(rc), path)
            file_list = self.get_filelist(fserver,result.fid)
            if len(file_list) != 1:
                rc = errno.ENOTEMPTY 
                raise OSError(rc, os.strerror(rc), path)
            name = get_basename(path)
            self.remove_name(fserver,result.parent_fid,name)
            fserver.remove(result.fid)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except NameNotFound, e:
            rc = errno.ENOENT
            raise OSError(rc, os.strerror(rc), path)

    def lsdir(self, path):
        "List contents of directory named path."
        try:
            fserver, result= self.lookup(path)
            self.check_dir(path, result.ftype)
            file_list = self.get_filelist(fserver, result.fid)
            fid_list = []
            for fid, ftype,fname in file_list:
                fid_list.append(fid)
            stat_list = fserver.stat_list(fid_list)
            dir_list = []
            for i, fattr in enumerate(stat_list):
                dir_list.append((file_list[i][2], fattr))
        except NameNotFound, e:
            rc = errno.ENOENT
            raise OSError(rc, os.strerror(rc), path)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), path)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)

        return dir_list

    def stat(self, path):
        "Return the latest file attributes for path."
        try:
            fserver, result = self.lookup(path)
            fattr = fserver.stat(result.fid)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), path)
        except NameNotFound, e:
            rc = errno.ENOENT
            raise OSError(rc, os.strerror(rc), path)
        return fattr

    def remove(self, path):
        "Remove a file named path."
        try:
            # get fid
            fserver, result = self.lookup(path)
            if result.ftype != FTYPE_FILE:
                if result.ftype == FTYPE_DIR:
                    rc = errno.EISDIR
                else:
                    rc = errno.ENXIO
                raise OSError(rc, os.strerror(rc), path)
            # update directory file and remove file
            name = get_basename(path)
            lock_tid = self.lock_file(result.fid, 'write')
            try:
                self.remove_name(fserver,result.parent_fid,name)
                fserver.remove(result.fid)
            finally:
                # unlock file
                self.unlock(lock_tid)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except NameNotFound, e:
            rc = errno.ENOENT
            raise OSError(rc, os.strerror(rc), path)
  
    def open(self, path, mode='r', tid=None):
        """This is the open method call by users of our proxy
        it behaves like the python open() function.
        """ 
   
        # first verify some basic mode values
        if 'w' in mode:
            truncate = True
            lock_mode = 'write'
        elif 'a' in mode:
            truncate = False
            lock_mode = 'write'
        elif 'r' in mode:
            truncate = False
            lock_mode = 'read'
        else:
            raise ValueError("invalid mode: %r" % mode)

        # locate the file (create if necessary)
        try:
            fserver,result = self.lookup(path)
            fid = result.fid
            ftype = result.ftype
        except NameNotFound as e:
            print 'NameNotFound', e
            if 'r' in mode:
                # requested read mode but file does not exist
                rc = errno.ENOENT
                raise OSError(rc, os.strerror(rc), path)
            try:
                # create new file and add its to directory file
                dirname = get_dirname(path)
                fserver, result = self.lookup(dirname)
                self.check_dir(dirname, result.ftype)
                name = get_basename(path)
                ftype = FTYPE_FILE
                fid,fattr = fserver.create(ftype)
                self.add_name(fserver, result.fid, name, fid, ftype)
            except NameExists, e:
                # race condition (let the other win)
                fserver.remove(new_fid)
                rc = errno.EACCES 
                raise OSError(rc, os.strerror(rc), path)
            except RequestError, e:
                raise OSError(e.rc, os.strerror(e.rc), path)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), path)

        # check path points to an actual file
        if ftype != FTYPE_FILE:
            if result.ftype == FTYPE_DIR:
                rc = errno.EISDIR
            else:
                rc = errno.ENXIO
            raise OSError(rc, os.strerror(rc), path)

        # lock the file - use given tid value if set
        new_tid = self.lock_file(fid, lock_mode, tid)

        # if caller passed us a tid we wont unlock (upto caller)
        if new_tid == tid:
            new_tid = None

        # call our interal open to do the rest
        return self.open_file(fserver, fid, ftype, new_tid, mode, truncate)

    #
    # File locking + Transaction support
    #

    def gen_tid(self):
        "Generate a uniqute transaction id."
        return str(uuid.uuid1())

    def lock_file(self, fid, mode, tid = None):
        """Acquire lock on given file identifer.
        We support READ, WRITE and WATCH locks.
        Returns a lock transaction id (tid) if successful.
        """
        if mode == 'read':
            lock_type = LOCK_READ
        elif mode == 'write':
            lock_type = LOCK_WRITE
        elif mode == 'watch':
            lock_type = LOCK_WATCH
        else:
            raise ValueError("invalid lock mode: %r" % mode)

        # locate lock server
        dserver = self.get_dserver()
        if not tid:
            # not part of a multi lock so use our own
            tid = self.gen_tid()

        # acquire lock
        try:
            result = dserver.trans_lock(lock_type, fid, tid)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        
        return tid

    def unlock(self, tid):
        "Release lock for given tid."

        # locate lock server
        dserver = self.get_dserver()

        # release lock
        try:
            result = dserver.trans_unlock(tid)
        except ConnError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)
        except RequestError, e:
            raise OSError(e.rc, os.strerror(e.rc), e.reason)

    def lock_path(self, path, mode, tid = None):
        "Acquire lock on given pathname."
        fserver,result = self.lookup(path)
        return self.lock_file(result.fid, mode, tid)


class Console(cmd.Cmd):
    """Admin tool that provides a command line interface for accessing the DFS
    """

    # internal state
    prompt = 'dfs$ '
    proxy = None
    locks = [] 
    cwd = '/'

    #
    # convert any relative path to absolute path
    #
    def get_path(self, line):
        fname = line.strip() 
        if fname.startswith('/'):
            # absolute
            path = fname
        else:
            # relative
            path = self.cwd
            if len(fname) > 0:
                # add suffix
                path = path + '/' + fname
        return path

    def do_greet(self, line):
        print "hello"

    def do_EOF(self, line):
        return True
    
    def do_pwd(self, line):

        print self.cwd

    def do_cd(self, line):

        path = line.strip()
        if path == '/':
            self.cwd = '/'
            return
        
        if path == '..':
            cwd = get_dirname(self.cwd)
        elif path.startswith('/'):
            cwd = path
        else:
            cwd = self.cwd + '/' + path

        try:
            handle = self.proxy.lookup_path(cwd)
            self.cwd = cwd
        except OSError, e:
            print e

    def do_lookup(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            handle = self.proxy.lookup_path(path)
            print 'handle is', handle
        except OSError, e:
            print e

    def do_mkdir(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
           self.proxy.mkdir(path)
        except OSError, e:
            print e

    def do_rmdir(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            self.proxy.rmdir(path)
        except OSError, e:
            print e

    def do_ls(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return

        try:
            alist = self.proxy.lsdir(path)
            for name,fattr in alist:
                print '[%s] %8d %s' %( ftype2str[fattr.ftype], fattr.size, name)
        except OSError, e:
            print e

    def do_stat(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            stat = self.proxy.stat(path)
            print stat.ftype, stat.mtime, stat.size, stat.fid, stat.raw_path
        except OSError, e:
            print e

    def do_rm(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            self.proxy.remove(path)
        except OSError, e:
            print e

    def do_cat(self, line):
        path = self.get_path(line)
        if len(path) == 0:
            print 'Missing path'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            f  = self.proxy.open(path, 'r')
            data = f.read()
            f.close()
            print data
        except OSError, e:
            print e

    def do_cpin(self, line):
        if not self.proxy:
            print 'Not connected'
            return

        fields = line.split(' ')
        if len(fields) != 2:
            print 'Missing src dst'
            return

        src_path = fields[0]
        dst_path = self.get_path(fields[1])

        fsrc = None
        fdst = None

        try:
            fsrc = io.open(src_path, 'rb')
            fdst = self.proxy.open(dst_path,'w')
            shutil.copyfileobj(fsrc,fdst)
        except OSError, e:
            print e
        except IOError, e:
            print e
        finally:
            if fdst: 
                fdst.close()
            if fsrc: 
                fsrc.close()

    def do_cpout(self, line):
        if not self.proxy:
            print 'Not connected'
            return

        fields = line.split(' ')
        if len(fields) != 2:
            print 'Missing src dst'
            return

        src_path = self.get_path(fields[0])
        dst_path = fields[1]

        fsrc = None
        fdst = None

        try:
            fsrc  = self.proxy.open(src_path, 'rb')
            fdst = io.open(dst_path, 'wb')
            shutil.copyfileobj(fsrc,fdst)
        except OSError, e:
            print e
        except IOError, e:
            print e
        finally:
            if fdst: 
                fdst.close()
            if fsrc: 
                fsrc.close()

    def do_fslist(self, line):

        if not self.proxy:
            print 'Not connected'
            return

        try:
            dserver = self.proxy.get_dserver()
            config = dserver.get_diraddrs()
            for src_dir, addr in config.iteritems():
                print src_dir, addr2str(addr)
        except ConnError, e:
            print e
        except RequestError, e:
            print e
        except OSError, e:
            print e

    def do_fsadd(self, line):
            
        if not self.proxy:
            print 'Not connected'
            return

        fields = line.strip().split(' ')
        if len(fields) != 2:
            print 'Missing src_dir host:port'
            return

        src_dir = fields[0]
        if not src_dir.startswith('/'):
            print 'src_dir must begin with a slash'

        host_port = fields[1].strip().split(':',1)
        if len(host_port) != 2:
            print 'Missing port number'
            return
        
        try:
            src_dir = fields[0]
            fsaddr = (host_port[0], int(host_port[1]))
            dserver = self.proxy.get_dserver()
            dserver.add_diraddr(src_dir, fsaddr)
        except ConnError, e:
            print e
        except RequestError, e:
            print e
        except OSError, e:
            print e

    def do_fsdel(self, line):
            
        if not self.proxy:
            print 'Not connected'
            return

        src_dir = line.strip()
        if len(src_dir) == 0:
            print 'Missing src_dir'
            return

        try:
            dserver = self.proxy.get_dserver()
            dserver.del_diraddr(src_dir)
        except ConnError, e:
            print e
        except RequestError, e:
            print e
        except OSError, e:
            print e

    def do_showlocks(self, line):
        for tid in self.locks:
            print tid
        print len(self.locks), 'locks found'

    def do_lock(self, line):
        fields = line.split(' ')
        if len(fields) < 2:
            print 'Missing path + lock_mode'
            return
        if not self.proxy:
            print 'Not connected'
            return

        path = self.get_path(fields[0])
        mode = fields[1]

        try:
            tid = self.proxy.lock_path(path, mode)
            print mode,'lock acquired for tid',tid
            self.locks.append(tid)
        except OSError, e:
            print e

    def do_unlock(self, line):
        tid = line.strip()
        if len(tid) == 0:
            print 'Missing tid'
            return
        if not self.proxy:
            print 'Not connected'
            return
        try:
            self.proxy.unlock(tid)
            print 'lock released for tid',tid
            self.locks.remove(tid)
        except ValueError:
            pass
        except OSError, e:
            print e

    def shutdown(self):
        if self.proxy:
            self.proxy.shutdown()
            self.proxy = None

    def do_connect(self, line):
        fields = line.split()
        if len(fields) !=2:
            print 'Missing address port'
            return
        addr = (fields[0], int(fields[1]))
        self.shutdown()
        self.proxy = ClientProxy(addr)

    def do_quit(self, line):
        sys.exit(0)

if __name__ == '__main__':
    console = Console()   
    if len(sys.argv) == 3:
        console.do_connect('%s %s' %(sys.argv[1], sys.argv[2]))
    console.cmdloop()
